一般调优都有3个步骤:
性能监控,需要一些系统工具来检测系统的运行状况,命令行下的jstack,jstat, 界面下的Jconsole,VisualVM
性能分析,当发现出问题了,需要分析造成问题的原因,如CPU过载或空转,FullGC时间过长等
性能调优,分析好出问题的原因后,需要采取行动,如定位代码的问题,优化代码,或者系统重新配置等
性能分析
CPU分析
当程序响应很慢时,可以使用top,vmstat,ps等命令查看CPU的使用率是否有异常,一般CPU忙时,有几种情况:
- 线程无限循环
- 频繁GC
- 线程切换频繁
内存分析
这里可分为堆外内存和堆内内存。
堆外内存
堆外内存主要是JNI、Deflater/Inflater、DirectByteBuffer(nio中会用到)使用的工具查看系统的物理内存,如果使用较高,则要考虑加内存,使用过少,考虑多分一些内存给堆内的。
堆内内存
此部分内存为Java应用主要的内存区域。当出现频繁GC或OOM时,表示内存堆内内存存在问题,使用jmap或者生产堆转储快照的方式分析。当出现问题时,应注意一下几个方面:
- 创建的对象:这个是存储在堆中的,需要控制好对象的数量和大小,尤其是大的对象很容易进入老年代
- 全局集合:全局集合通常是生命周期比较长的,因此需要特别注意全局集合的使用
- 缓存:缓存选用的数据结构不同,会很大程序影响内存的大小和gc
- ClassLoader:主要是动态加载类容易造成永久代内存不足
- 多线程:线程分配会占用本地内存,过多的线程也会造成内存不足
IO分析
vmstat来查看本地文件io的状况,由此可以判定io繁忙状况
使用的是netstat工具查看网络I/O,当time_wait或者close_wait连接过多时,会影响应用的相应速度。
性能调优
CPU调优
- 不要存在一直运行的线程(无限while循环),可以使用sleep休眠一段时间。这种情况普遍存在于一些pull方式消费数据的场景下,当一次pull没有拿到数据的时候建议sleep一下,再做下一次pull。
- 轮询的时候可以使用wait/notify机制
- 避免循环、正则表达式匹配、计算过多,包括使用String的format、split、replace方法(可以使用apache的commons-lang里的StringUtils对应的方法),使用正则去判断邮箱格式(有时候会造成死循环)、序列/反序列化等。
- 结合jvm和代码,避免产生频繁的gc,尤其是full GC。
内存调优
合理设置各个代的大小。避免新生代设置过小(不够用,经常minor gc并进入老年代)以及过大(会产生碎片),同样也要避免Survivor设置过大和过小。
选择合适的GC策略。需要根据不同的场景选择合适的gc策略。这里需要说的是,cms并非全能的。除非特别需要再设置,毕竟cms的新生代回收策略parnew并非最快的,且cms会产生碎片。此外,G1直到jdk8的出现也并没有得到广泛应用,并不建议使用。
jvm启动参数配置-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:[log_path],以记录gc日志,便于排查问题。
关于年轻代和年老代的选择:
年轻代大小选择:响应时间优先的应用,尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生gc的频率是最小的。同时,也能够减少到达年老代的对象。吞吐量优先的应用,也尽可能的设置大,因为对响应时间没有要求,垃圾收集可以并行进行,建议适合8CPU以上的应用使用。
年老代大小选择:响应时间优先的应用,年老代一般都是使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。
而代码上面的优化,可以从以下几个方面考虑:
- 避免保存重复的String对象,同时也需要小心String.subString()与String.intern()的使用
- 尽量不要使用finalizer
- 释放不必要的引用:ThreadLocal使用完记得释放以防止内存泄漏,各种stream使用完也记得close。
- 使用对象池避免无节制创建对象,造成频繁gc。但不要随便使用对象池,除非像连接池、线程池这种初始化/创建资源消耗较大的场景,
- 缓存失效算法,可以考虑使用SoftReference、WeakReference保存缓存对象
- 谨慎热部署/加载的使用,尤其是动态加载类等
- 不要用Log4j输出文件名、行号,因为Log4j通过打印线程堆栈实现,生成大量String。此外,使用log4j时,建议此种经典用法,先判断对应级别的日志是否打开,再做操作,否则也会生成大量String。
IO调优
文件IO上需要注意:
- 考虑使用异步写入代替同步写入,可以借鉴redis的aof机制。
- 利用缓存,减少随机读
- 尽量批量写入,减少io次数和寻址
- 使用数据库代替文件存储
网络IO上需要注意:
- 和文件IO类似,使用异步IO、多路复用IO/事件驱动IO代替同步阻塞IO
- 批量进行网络IO,减少IO次数
- 使用缓存,减少对网络数据的读取
- 使用协程: Quasar
Full GC
减少Full GC的频率
减少Young GC和Full GC的时间
GC 调优对高并发大数据量交互的应用还是很有必要的,尤其是默认 JVM 参数通常不满足业务需求,需要进行专门调优。由于系统堆设置较大,Full GC 一次暂停应用时间会较长,这对线上实时服务影响较大
最好的GC算法是Concurrent Mark Sweep(CMS垃圾回收),特别是对于Web服务端程序。因为低延迟是非常重要的。
新生代(Young generation)的空间太小,导致有一些本应该可以很快就被回收的对象被放到了老生代(Old generation)里,导致老生代上涨很快,频繁
Full GC
。增加新生代的大小,修改后,Yong GC都在10ms下,达到了想要的效果。建议新生代占整个堆的1/4~1/2full gc还是比较多,且持续时间较长。ygc的对象进入老年代,是按照年龄计算的,这个年龄默认是15,但是是动态调整的,所以加这个参数,再观察一下。 查看日志可以看出,对象动态调整年龄是4
为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到Max Tenuring Threshold才能晋升老年代如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到Max Tenuring Threshold中要求的年龄。
因此,需要调大survivor,增加SurvivorRatio = 6
如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于 GC 来说会是一场灾难。原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路.因此,控制直接进入Old generation的threshold也是很重要的
总结主要调优参数:
设定堆内存大小
-Xms:启动JVM时的堆内存空间。
-Xmx:堆内存最大限制。
设定新生代大小
新生代不宜太小,否则会有大量对象涌入老年代。
-XX:NewRatio:新生代和老年代的占比。
-XX:NewSize:新生代空间。
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比。
-XX:MaxTenuringThreshold:对象进入老年代的年龄阈值。
设定老生代大小
老生代太大,每次Full GC的时间会很长,太小,则Full GC会比较频繁。
设定垃圾回收器
年轻代:-XX:+UseParNewGC。
老年代:-XX:+UseConcMarkSweepGC。
CMS可以将STW时间降到最低,但是不对内存进行压缩,有可能出现“并行模式失败”。比如老年代空间还有300MB空间,但是一些10MB的对象无法被顺序的存储。这时候会触发压缩处理,但是CMS GC模式下的压缩处理时间要比Parallel GC长很多。
G1采用”标记-整理“算法,解决了内存碎片问题,建立了可预测的停顿时间类型,能让使用者指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。